Thread pool is a powerful way to manage a group of threads. There are many reasons to use a thread pool. For example, 1) reducing the waste of resources: by reusing existing threads we can reduce the overhead of recreating threads. 2) Centralized manageability of threads. Thread pool provides capabilities to help us monitor and optimize threads by indicating different parameters.
In Java, thread pool is implemented in ThreadPoolExecutor class. There are four core concepts in ThreadPoolExecutor.
Work Queue
A work queue is used as a “buffer” to hold submitted tasks in a thread pool. When the size of a work queue reaches its capacity, we cannot submit new tasks to a thread pool anymore. Generally, any implementation of BlockingQueue in Java can be used as a work queue.
Core Pool Size
The core pool size is the number of threads that should be alive in a thread pool. The core threads are not terminated by the thread pool even if they are idle.
Maximum Pool Size
This is the largest number of threads that we can create in a thread pool. If the work queue is full but the total number of running threads is less than the maximum pool size, a new thread will be created to handle the new submitted task.
Keep-alive Time
If the number of threads in a thread pool is larger than the core pool size, extra threads will be terminated if they are idle for keep-alive time.
The following flow chart shows the workflow of a thread pool in Java.
Executors is a utility class in Java with many static methods that create different thread pools. For example, newCachedThreadPool method creates a thread pool which initiates new threads when needed, newFixedThreadPool(int nThreads) creates a thread pool which reuses a number of threads.
Generally, Cached Thread Pool is better in handling a lot of short-lived asynchronous tasks, while Fixed Thread Pool is better when we want to limit the usage of computing resources.
Using Executors we can create different thread pools with very few parameters. It hides many details in the creation of thread pools.
ThreadPoolExecutor provides four constructors. A widely-used constructor is
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
Other constructors require RejectionHandler and / or ThreadFactory as input parameters.
As we see, since creating a ThreadPoolExecutor requires more input parameters, we need to understand the internal workflow to better use ThreadPoolExecutor. Creating a thread pool through static methods in Executors requires only a few parameters. Some “lazy” developers (like me) may think using Executors class is more convenient at first glance. But think twice, why is using Executors to create a thread pool not recommended in some companies?
To answer the question, let us take a look at the source code of Executors.
You can find the source code of Executors here - https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/concurrent/Executors.java .
The newCachedThreadPool method is implemented as below.
1
2
3
4
5
6
7
8
9
10
11
12
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60 L, TimeUnit.SECONDS,
new SynchronousQueue < Runnable > ());
}
// The newFixedThreadPool method is implemented as below.
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0 L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue < Runnable > ());
}
As we see, both newCachedThreadPool and newFixedThreadPool methods return a ThreadPoolExecutor instance with some default parameters.
The newCachedThreadPool method creates a thread pool with Integer.MAX_VALUE as the maximum number of threads. Also there is no limit to the capacity of the work queue. We may get memory overflow without restrictions on the number of threads and the capacity of work queue in working environment.
The newFixedThreadPool method creates a thread pool without limit on the capacity of work queue. Also the maximum number of threads is the same as the number of core threads. These settings may cause memory overflow in the working environment, or cannot make full use of computing resources.
Now you understand why using ThreadPoolExecutor directly is recommended.
ThreadPoolExecutor provides two methods for running tasks - execute and submit. Execute method takes tasks which have no return values, while submit method takes tasks with return values.
There are two methods to shutdown a ThreadPoolExecutor - shutdown() and shutdownNow(). The difference between them is the shutdownNow method tries to interrupt running threads while shutdown method does not.
The following example shows the basic operations of ThreadPoolExecutor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public class Demo { public static void main(String[] args) { BlockingQueue < Runnable > queue = new ArrayBlockingQueue < Runnable > (5); RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 3, 0, TimeUnit.SECONDS, queue, handler); for (int i = 1; i < 10; i++) { pool.execute(() - > { System.out.println("Running task " + i); }); } pool.shutdown(); } }